package emulator.hardware.io;

import emulator.hardware.HwByte;
import emulator.hardware.bits.SharedBit;
import emulator.hardware.bus.Bus;
import emulator.hardware.clock.Clock;
import emulator.hardware.clock.ClockHandle;
import emulator.hardware.debug.BusWatchException;
import emulator.hardware.io.registers6522.*;
import emulator.hardware.memory.UnmappedMemoryException;

public class Via6522 extends Thread implements Bus {

	private Register6522 [] registers = new Register6522 [16];
	private ClockHandle clock = null;
	private SharedBit irq_out;
	private Port6522 portA = new Port6522();
	private Port6522 portB = new Port6522();
	private Counter timer1 = new Counter();
	private Counter timer2 = new Counter();
	private HwByte interrupt_flags = new HwByte();
	private HwByte interrupt_mask = new HwByte();
	private int timer1_latch = 0;
	private int timer2_latch = 0;
	private int shift_register = 0;
	private AuxiliaryControlRegister aux_control = new AuxiliaryControlRegister();
	private PortControlRegister port_control = new PortControlRegister();
	private boolean timer1_set = false;
	private boolean timer2_set = false;
	private boolean last_ca1 = false;
	private boolean last_ca2 = false;
	private boolean last_cb1 = false;
	private boolean last_cb2 = false;

	public Via6522()
	{
		super();
		registers[0] = new ORB();
		registers[1] = new ORA();
		registers[2] = new DDRB();
		registers[3] = new DDRA();
		registers[4] = new T1CL();
		registers[5] = new T1CH();
		registers[6] = new T1LL();
		registers[7] = new T1LH();
		registers[8] = new T2CL();
		registers[9] = new T2CH();
		registers[10] = new SR();
		registers[11] = new ACR();
		registers[12] = new PCR();
		registers[13] = new IFR();
		registers[14] = new IER();
		registers[15] = new ORA2();
	}
	
	public void attach(Clock clock, SharedBit irq_out)
	{
		if (clock != null)
			this.clock = clock.acquireHandle();
		this.irq_out = irq_out;
	}
	
	public void detach()
	{
		this.clock = null;
	}

	/* (non-Javadoc)
	 * @see java.lang.Thread#run()
	 */
	@Override
	public void run() {
		for (;;)
		{
			clock.tick();
			synchronized(this)
			{
				handleTimer1();
				handleTimer2();
				handlePortA();
				handlePortB();
				evaluateInterrupts();
				irq_out.setValue(!interrupt_flags.getBit(7));
			}
		}
	}

	private void handleTimer1()
	{
		if (timer1.countDown() == 0)
		{
			timer1.load(timer1_latch);
			if ((aux_control.getTimer1Mode() & 1) == 0)
			{
				if (timer1_set)
				{
					interrupt_flags.setBit(6, true);
					timer1_set = false;
					if ((aux_control.getTimer1Mode() & 2) != 0)
						portB.getOutPortBit(7).setValue(true);
				}
			}
			else
			{
				interrupt_flags.setBit(6, true);
				if ((aux_control.getTimer1Mode() & 2) != 0)
					portB.getOutPortBit(7).toggle();
			}
		}
	}

	private void startTimer1()
	{
		timer1.load(timer1_latch);
		timer1_set = true;
		if (aux_control.getTimer1Mode() == 2)
			portB.getOutPortBit(7).setValue(false);
	}

	private void handleTimer2()
	{
		int count = 1;
		if (aux_control.getTimer2Mode() == false)
			count = timer2.countDown();
		else
			; /* TODO: PB6 triggers countdown */
		if (count == 0)
		{
			timer2.load(timer2_latch);
			if (timer2_set)
			{
				interrupt_flags.setBit(5, true);
				timer2_set = false;
			}
		}
	}

	private void startTimer2()
	{
		timer2.load(timer2_latch);
		timer2_set = true;
	}

	private void handlePortA() {
		evaluatePort(portA,	port_control.getIrq0Mode(), port_control.getIrq1Mode(),
							0, 1, last_ca1, last_ca2);
		last_ca1 = portA.getControlBit(0).getValue();
		last_ca2 = portA.getControlBit(1).getValue();
	}

	private void handlePortB() {
		evaluatePort(portB,	port_control.getIrq3Mode(), port_control.getIrq4Mode(),
							3, 4, last_cb1, last_cb2);
		last_cb1 = portB.getControlBit(0).getValue();
		last_cb2 = portB.getControlBit(1).getValue();
	}

	private void evaluatePort(Port6522 port, int irq0_mode, boolean irq1_mode,
			int irq0_bit, int irq1_bit, boolean last_ctrl1, boolean last_ctrl2)
	{
		port.pre();
		boolean ctrl1 = port.getControlBit(0).getValue();
		boolean ctrl2 = port.getControlBit(1).getValue();
		boolean ctrl1_changed = (ctrl1 != last_ctrl1);
		boolean ctrl2_changed = (ctrl2 != last_ctrl2);
		if (ctrl1_changed && ctrl1 == irq1_mode) {
			interrupt_flags.setBit(irq1_bit, true);
		}
		
		switch (irq0_mode)
		{
		case 0: // 000
			if (ctrl2_changed && ctrl2 == false)
				interrupt_flags.setBit(irq0_bit, true);
			break;
		case 1: // 100
			if (ctrl2_changed && ctrl2 == false)
				interrupt_flags.setBit(irq0_bit, true);
			break;
		case 2: // 010
			if (ctrl2_changed && ctrl2 == true)
				interrupt_flags.setBit(irq0_bit, true);
			break;
		case 3: // 110
			if (ctrl2_changed && ctrl2 == true)
				interrupt_flags.setBit(irq0_bit, true);
			break;
		case 4: // 001
			if (ctrl1_changed && ctrl1 == true && ctrl2 == false)
				port.getControlBit(1).setValue(true);
			break;
		case 5: // 101
			if (!ctrl2_changed && ctrl2 == false)
				port.getControlBit(1).setValue(true);
			break;
		case 6: // 011
			if (ctrl2 == true)
				port.getControlBit(1).setValue(false);
			break;
		case 7: // 111
			if (ctrl2 == false)
				port.getControlBit(1).setValue(true);
			break;
		}
		port.post();
	}

	@Override
	synchronized public int read(int address) throws BusWatchException,
			UnmappedMemoryException {
		return registers[address & 0x0f].read(this);
	}

	@Override
	synchronized public void write(int address, int data) throws BusWatchException,
			UnmappedMemoryException {
		registers[address & 0x0f].write(this, data & 0xFF);
	}
	
	private void evaluateInterrupts() {
		boolean interrupt_pending = ((interrupt_flags.getNumber() & interrupt_mask.getNumber()) & 0x7f) > 0;
		interrupt_flags.setBit(7, interrupt_pending);
	}


	// R/W Registers

	public int readORB() {
		resetPortBInterrupt();
		return portB.readPort();
	}

	public void writeORB(int value) {
		portB.writePort(value);
		resetPortBInterrupt();
	}

	private void resetPortBInterrupt() {
		interrupt_flags.setBit(3, false);
		interrupt_flags.setBit(4, false);
	}

	public int readORA() {
		resetPortAInterrupt();
		return portA.readPort();
	}

	public void writeORA(int value) {
		portA.writePort(value);
		resetPortAInterrupt();
	}
	
	private void resetPortAInterrupt() {
		interrupt_flags.setBit(0, false);
		interrupt_flags.setBit(1, false);
	}

	public int readDDRB() {
		return portB.getPortMask();
	}

	public void writeDDRB(int value) {
		portB.setPortMask(value);
	}

	public int readDDRA() {
		return portA.getPortMask();
	}

	public void writeDDRA(int value) {
		portA.setPortMask(value);
	}

	public int readT1CL() {
		interrupt_flags.setBit(6, false);
		return timer1.getValue() & 0xff;
	}

	public void writeT1CL(int value) {
		timer1_latch = (timer1_latch & 0xff00) | value;
	}

	public int readT1CH() {
		return ((timer1.getValue() >> 8) & 0xff);
	}

	public void writeT1CH(int value) {
		timer1_latch = (timer1_latch & 0x00ff) | (value << 8);
		interrupt_flags.setBit(6, false);
		startTimer1();
	}

	public int readT1LL() {
		return timer1_latch & 0xff;
	}

	public void writeT1LL(int value) {
		timer1_latch = (timer1_latch & 0xff00) | value;
	}

	public int readT1LH() {
		return (timer1_latch >> 8) & 0xff;
	}

	public void writeT1LH(int value) {
		timer1_latch = (timer1_latch & 0x00ff) | (value << 8);
	}

	public int readT2CL() {
		interrupt_flags.setBit(5, false);
		return timer2.getValue() & 0xff;
	}

	public void writeT2CL(int value) {
		timer2_latch = (timer2_latch & 0xff00) | value;
	}

	public int readT2CH() {
		return (timer2.getValue() >> 8) & 0xff;
	}

	public void writeT2CH(int value) {
		timer2_latch = (timer2_latch & 0x00ff) | (value << 8);
		interrupt_flags.setBit(5, false);
		startTimer2();
	}

	public int readSR() {
		interrupt_flags.setBit(2, false);
		return shift_register;
	}

	public void writeSR(int value) {
		interrupt_flags.setBit(2, false);
		shift_register = value;
	}

	public int readACR() {
		return aux_control.getValue();
	}

	public void writeACR(int value) {
		aux_control.setValue(value);
	}

	public int readPCR() {
		return port_control.getValue();
	}

	public void writePCR(int value) {
		port_control.setValue(value);
	}

	public int readIFR() {
		return (int) interrupt_flags.getNumber();
	}

	public void writeIFR(int value) {
		for (int i = 0; i < 7; i++)
			if ((value & (1 << i)) != 0)
				interrupt_flags.setBit(i, false);
		evaluateInterrupts();
	}

	public int readIER() {
		return (int) (interrupt_mask.getNumber() | 0x80);
	}

	public void writeIER(int value) {
		for (int i = 0; i < 7; i++)
			if ((value & (1 << i)) != 0)
				interrupt_mask.setBit(i, (value & 0x80) != 0);
	}

	public int readORA2() {
		return portA.readPort();
	}

	public void writeORA2(int value) {
		portA.writePort(value);
	}

	public Port6522 getPortB() {
		return portB;
	}

	public Port6522 getPortA() {
		return portA;
	}

}
